﻿using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using SDCloudSolution.SDCloud.Entities.Models.Data;
using System.Reflection;
using System.ComponentModel.DataAnnotations.Schema;
using System;
using SDCloudSolution.SDCloud.Entities.Models;

namespace SDCloudSolution.SDCloud.DataLayer.ModelData
{
    public static class MongoDbConverter
    {
        public static class MongoFieldNames
        {
            public static class ModelExecutionMetadata
            {
                public const string CreateDateUtc = "CreateDateUtc";

                public const string ModelId = "ModelId";

                public const string ModelExecutionId = "ModelExecutionId";

                public const string ResultsSizeInBytes = "ResultsSizeInBytes";
            }

            public static class ModelResultData
            {
                public const string Id = "_id";

                public const string Metadata = "Metadata";

                public const string Columns = "Columns";

                public const string TimeStart = "TimeStart";

                public const string TimeEnd = "TimeEnd";

                public const string TimeFrames = "TimeFrames";
            }

            public static class ModelResultColumn
            {
                public const string Name = "Name";

                public const string DisplayName = "DisplayName";

                public const string UnitName = "UnitName";
            }

            public static class ModelResultTimeFrame
            {
                public const string ModelId = "ModelId";

                public const string ModelExecutionId = "ModelExecutionId";

                public const string Time = "Time";

                public const string TimeDelta = "TimeDelta";

                public const string Values = "Values";
            }
        }

        public static BsonDocument ToBsonDocument(this ModelExecutionMetadataEntity entity)
        {
            var document = new BsonDocument()
                .Add(MongoFieldNames.ModelExecutionMetadata.CreateDateUtc, entity.CreationDateUtc)
                .Add(MongoFieldNames.ModelExecutionMetadata.ModelId, entity.ModelId)
                .Add(MongoFieldNames.ModelExecutionMetadata.ModelExecutionId, entity.ModelRunId)
                .Add(MongoFieldNames.ModelExecutionMetadata.ResultsSizeInBytes, entity.ResultSizeInBytes);

            return document;
        }

        public static BsonDocument ToBsonDocument(this ModelResultColumnEntity columnEntity)
        {
            var document = new BsonDocument()
                .Add(MongoFieldNames.ModelResultColumn.Name, columnEntity.Name)
                .Add(MongoFieldNames.ModelResultColumn.DisplayName, columnEntity.DisplayName ?? columnEntity.Name)
                .Add(MongoFieldNames.ModelResultColumn.UnitName, columnEntity.UnitName ?? string.Empty);

            return document;
        }

        public static BsonDocument ToBsonDocument(IDictionary<string, double> dictionary)
        {
            var document = new BsonDocument();

            document.AddRange(dictionary.Select(pair => new BsonElement(pair.Key, pair.Value)));

            return document;
        }

        public static BsonDocument ToBsonDocument(this ModelResultTimeFrameEntity timeFrameEntity)
        {
            var document = new BsonDocument()
                .Add(MongoFieldNames.ModelResultTimeFrame.ModelId, timeFrameEntity.ModelId)
                .Add(MongoFieldNames.ModelResultTimeFrame.ModelExecutionId, timeFrameEntity.ModelExecutionId)
                .Add(MongoFieldNames.ModelResultTimeFrame.Time, timeFrameEntity.Time)
                .Add(MongoFieldNames.ModelResultTimeFrame.TimeDelta, timeFrameEntity.TimeDelta)
                .Add(MongoFieldNames.ModelResultTimeFrame.Values, ToBsonDocument(timeFrameEntity.Values));

            return document;
        }

        public static BsonDocument ToBsonDocument(this ModelResultDataEntity dataEntity)
        {
            var id = string.IsNullOrWhiteSpace(dataEntity.Id)
                ? ObjectId.GenerateNewId()
                : ObjectId.Parse(dataEntity.Id);

            dataEntity.Id = id.ToString();

            var columns = new BsonArray()
                .AddRange(dataEntity.Columns.Select(column => column.ToBsonDocument()));

            var document = new BsonDocument()
                .Add(MongoFieldNames.ModelResultData.Id, id)
                .Add(MongoFieldNames.ModelResultData.Metadata, dataEntity.Metadata.ToBsonDocument())
                .Add(MongoFieldNames.ModelResultData.TimeStart, dataEntity.TimeStart)
                .Add(MongoFieldNames.ModelResultData.TimeEnd, dataEntity.TimeEnd)
                .Add(MongoFieldNames.ModelResultData.Columns, columns);

            return document;
        }

        public static ModelResultColumnEntity ToColumnEntity(this BsonDocument document)
        {
            return new ModelResultColumnEntity
            {
                Name = document.GetValue(MongoFieldNames.ModelResultColumn.Name).AsString,
                DisplayName = document.GetValue(MongoFieldNames.ModelResultColumn.DisplayName).AsString,
                UnitName = document.GetValue(MongoFieldNames.ModelResultColumn.UnitName).AsString
            };
        }

        public static Dictionary<string, double> ToDictionaryValues(this BsonDocument document)
        {
            return document.Elements.ToDictionary(
                element => element.Name,
                element => element.Value.AsDouble
            );
        }

        public static ModelResultTimeFrameEntity ToTimeFrameEntity(this BsonDocument document)
        {
            return new ModelResultTimeFrameEntity
            {
                ModelId = document.GetValue(MongoFieldNames.ModelResultTimeFrame.ModelId).AsInt32,
                ModelExecutionId = document.GetValue(MongoFieldNames.ModelResultTimeFrame.ModelExecutionId).AsInt32,
                Time = document.GetValue(MongoFieldNames.ModelResultTimeFrame.Time).AsDouble,
                TimeDelta = document.GetValue(MongoFieldNames.ModelResultTimeFrame.TimeDelta).AsDouble,
                Values = document.GetValue(MongoFieldNames.ModelResultTimeFrame.Values).AsBsonDocument.ToDictionaryValues()
            };
        }

        public static ModelExecutionMetadataEntity ToMetadataEntity(this BsonDocument document)
        {
            return new ModelExecutionMetadataEntity
            {
                ModelId = document.GetValue(MongoFieldNames.ModelExecutionMetadata.ModelId).AsInt32,
                ModelRunId = document.GetValue(MongoFieldNames.ModelExecutionMetadata.ModelExecutionId).AsInt32,
                ResultSizeInBytes = document.GetValue(MongoFieldNames.ModelExecutionMetadata.ResultsSizeInBytes).AsInt64,
                CreationDateUtc = document.GetValue(MongoFieldNames.ModelExecutionMetadata.CreateDateUtc).ToUniversalTime()
            };
        }

        public static ModelResultDataEntity ToResultDataEntity(this BsonDocument document)
        {
            return new ModelResultDataEntity
            {
                Id = document.GetValue(MongoFieldNames.ModelResultData.Id).AsObjectId.ToString(),
                Metadata = document.GetValue(MongoFieldNames.ModelResultData.Metadata).AsBsonDocument.ToMetadataEntity(),
                TimeStart = document.GetValue(MongoFieldNames.ModelResultData.TimeStart).AsDouble,
                TimeEnd = document.GetValue(MongoFieldNames.ModelResultData.TimeEnd).AsDouble,
                Columns = document
                            .GetValue(MongoFieldNames.ModelResultData.Columns).AsBsonArray
                            .Select(value => value.AsBsonDocument.ToColumnEntity())
                            .ToArray()
            };
        }

        public static ModelMetadataEntity ToModelMetadataEntity(this BsonDocument document)
        {
            return new ModelMetadataEntity();
        }

        public static BsonDocument ToBsonDocumentByAttributes(object entity)
        {
            BsonDocument result = new BsonDocument();

            PropertyInfo[] properties = entity.GetType().GetProperties();

            properties.ToList().ForEach(property =>
            {
                ColumnAttribute columnAttribute =
                    (ColumnAttribute)property.GetCustomAttribute(typeof(ColumnAttribute));

                if (null != columnAttribute)
                {
                    //columnAttribute.Name
                    object propValue =
                        entity.GetType().GetProperty(property.Name).GetValue(entity, null);

                    if (propValue != null)
                    {
                        if (property.PropertyType.Equals(typeof(int)))
                        {
                            result.Add(columnAttribute.Name, (int)propValue);
                        }
                        else if (property.PropertyType.Equals(typeof(double)))
                        {
                            result.Add(columnAttribute.Name, (double)propValue);
                        }
                        else if (property.PropertyType.Equals(typeof(string)))
                        {
                            result.Add(columnAttribute.Name, (string)propValue);
                        }
                        else if (property.PropertyType.Equals(typeof(long)))
                        {
                            result.Add(columnAttribute.Name, (long)propValue);
                        }
                        else if (property.PropertyType.Equals(typeof(Dictionary<string, object>)))
                        {
                            BsonDocument subDoc = new BsonDocument();
                            subDoc.AddRange((Dictionary<string, object>)propValue);
                            result.Add(columnAttribute.Name, subDoc);
                        }
                    }

                }

            });

            return result;
        }

        public static object ToObjectFromBsonDocument(BsonDocument document, Type targetType)
        {
            object result = null;

            if (null != document)
            {
                try
                {
                    result = Activator.CreateInstance(targetType);
                }
                catch (Exception e)
                {
                    /* We failed to instantiate an object :( */
                }
            }

            if (null != result)
            {
                
                List<string> docFields = document.Names.ToList();
                docFields.ForEach(field =>
                {
                    PropertyInfo property = result.GetType().GetProperty(field);
                    if (null != property)
                    {
                        if (property.PropertyType.Equals(typeof(int)))
                        {
                            targetType.GetProperty(property.Name).SetValue(result, document[field].AsInt32);
                        }
                        else if (property.PropertyType.Equals(typeof(double)))
                        {
                            targetType.GetProperty(property.Name).SetValue(result, document[field].AsDouble);
                        }
                        else if (property.PropertyType.Equals(typeof(string)))
                        {
                            targetType.GetProperty(property.Name).SetValue(result, document[field].AsString);
                        }
                        else if (property.PropertyType.Equals(typeof(long)))
                        {
                            targetType.GetProperty(property.Name).SetValue(result, document[field].AsInt64);
                        }
                        else if (property.PropertyType.Equals(typeof(Dictionary<string, object>)))
                        {
                            BsonDocument subDoc = document[field].AsBsonDocument;
                            Dictionary<string, object> newDictionary = 
                                new Dictionary<string, object>();
                            subDoc.Names.ToList().ForEach(fieldName => {
                                newDictionary.Add(fieldName, subDoc.GetValue(fieldName));
                            });
                            targetType.GetProperty(property.Name).SetValue(result, newDictionary);
                        }
                    }
                });
            }

            return result;
        }
    }
}
